Skip to content

Lifting Content Up

Video Summary

I feel like this module so far has been a mix of eye-opening realizations, and the mind-melting implications of those realizations. This lesson is no exception. We're not introducing a complex API or doing anything radically different, but it is pretty darn confusing nevertheless!

Here's the scenario: we're building a news website, working on the page that shows a specific article. The component is given an articleSlug prop, a unique identifier for the current article.

The top-level component, ArticlePage, looks like this:

function ArticlePage({ articleSlug }) {
return (
<>
<Header />
<MainContent articleSlug={articleSlug} />
<footer>
Copyright “The News” Inc.
</footer>
</>
);
}

MainContent includes a floating sidebar with social sharing links, and the article itself:

function MainContent({ articleSlug }) {
return (
<main>
<aside>
<SocialSharingWidget />
</aside>
<Article articleSlug={articleSlug} />
</main>
);
}

And, finally, Article renders the title/image/content:

function Article({ articleSlug }) {
const article = useArticle(articleSlug);
if (!article) {
return (
<p>No Article found</p>
)
}
return (
<article>
<h2>{article.title}</h2>
<p className="subtitle">{article.subtitle}</p>
<img
className="article-img"
src={article.image.src}
alt={article.image.alt}
/>
<div className="article-content">
{article.content}
</div>
</article>
);
}

(In a real app, that useArticle custom hook would make a network request to fetch the data about the article, given its unique slug. In this contrived example, we return a hardcoded nonsense article about an escaped otter who becomes a detective.)

The thing that stands out to me about this code is that MainContent component. It funnels the articleSlug prop along, without actively using it.

In the last module, we saw how we can use React context to solve this problem. Essentially, we'd create a Provider at the top (inside ArticlePage), and consume the context in the Article component, bypassing MainContent altogether.

But hm, I wonder if there's a simpler option here… What if we could pass the articleSlug prop directly to <Article> without having to use context, and without threading it through a middle component?

Your mission in this lesson is to poke and prod at this example, and see if you can find a way to solve it.

This is a very open-ended challenge, so let's cover some ground rules. They're listed in the “Acceptance Criteria” below.

Spend a few minutes and see if you can refactor this code. As I said in the video, I don't expect you to be able to solve this! The value here is in spending a few minutes thinking about it, poking and prodding at the problem.

Acceptance Criteria:

  • Refactor the code so that Article receives the articleSlug prop directly: without using context, and without funnelling the prop through an intermediary component.
  • The DOM structure shouldn't change at all.
  • We can't remove a component altogether. For example, you're not allowed to “merge” ArticlePage and MainContent into a single component.
  • It isn't some fancy trick with custom hooks, you won't have to touch useArticle at all, or use any other custom hook.

Code Playground

import React from 'react';

import Header from './Header';
import SocialSharingWidget from './SocialSharingWidget';
import useArticle from './use-article.js';

function ArticlePage({ articleSlug }) {
return (
<>
<Header />
<MainContent articleSlug={articleSlug} />
<footer>
Copyright “The News” Inc.
</footer>
</>
);
}

function MainContent({ articleSlug }) {
return (
<main>
<aside>
<SocialSharingWidget />
</aside>
<Article articleSlug={articleSlug} />
</main>
);
}

function Article({ articleSlug }) {
const article = useArticle(articleSlug);
if (!article) {
return (
<p>No Article found</p>
)
}
return (
<article>
<h2>{article.title}</h2>
<p className="subtitle">{article.subtitle}</p>

<img
className="article-img"
src={article.image.src}
alt={article.image.alt}
/>

<div className="article-content">
{article.content}
</div>
</article>
);
}

export default ArticlePage;

Alright, let's break this down.

Video Summary

So we've been given some pretty onerous restrictions here. We're told that we can't use context, and we can't funnel the data through an intermediary component.

Our only real option, then, is for the <Article> element to be created by the ArticlePage component:

function ArticlePage({ articleSlug }) {
return (
<>
<Header />
<MainContent articleSlug={articleSlug} />
+ <Article articleSlug={articleSlug} />
<footer>
Copyright “The News” Inc.
</footer>
</>
);
}
function MainContent({ articleSlug }) {
return (
<main>
<aside>
<SocialSharingWidget />
</aside>
- <Article articleSlug={articleSlug} />
</main>
);
}

When we move the element over like this, it does solve this problem, but it introduces a new one: we've changed the DOM structure!

Before, <article> was within <main>:

<div id="root">
<header>...</header>
<main>
<aside>...</aside>
<article>...</article>
</main>
<header>footer</header>
</div>

Now, though, it exists as a sibling:

<div id="root">
<header>...</header>
<main>
<aside>...</aside>
</main>
<article>...</article>
<header>footer</header>
</div>

In addition to (slightly) breaking our layout, it also means that we've changed the semantics of our document, and not for the better.

What I want is for the <article> DOM node to still be rendered within <main>, but for the <Article> React element to be created within ArticlePage.

We can do exactly this by leveraging the children prop:

function ArticlePage({ articleSlug }) {
return (
<>
<Header />
{/*
Move <Article> so that it's a child of
the <MainContent> element:
*/}
<MainContent>
<Article articleSlug={articleSlug} />
</MainContent>
<footer>
Copyright “The News” Inc.
</footer>
</>
);
}
function MainContent({ children }) {
return (
<main>
<aside>
<SocialSharingWidget />
</aside>
{/* Create a “slot” for arbitrary React elements */}
{children}
</main>
);
}

By structuring things in this way, we've preserved the exact same DOM structure as we had initially, but we've changed the React structure so that <Article> is being created by the ArticlePage component.

This strategy is known as lifting content up.

Personally, this strategy hurts my brain a little bit. It feels like it shouldn't work! But somehow it does??

Part of the issue is that we often think in terms of parents and children. That's how we think about DOM nodes. But there's another important relationship when it comes to React: owners and ownees.

This is a fundamental concept; we see this relationship all over the React codebase.

When a React element is created within a component, we say that the component “owns” that element. It decides what the props should be.

And so, this restructure hasn't changed the parent/child relationship in terms of the DOM, but it has changed which component owns the <Article> element! By lifting the element up so that it's owned by ArticlePage, we're changing which component is responsible for setting its props, and neatly solving the problem at hand.